package com.beemans.common.ui.views.sticky

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.util.SparseArray
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.util.forEach
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.beemans.common.ui.views.MyNestedScrollView
import java.lang.ref.WeakReference
import kotlin.math.min

/**
 * @author tiamosu
 * @date 2021/8/30.
 */
class HeaderScrollView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MyNestedScrollView(context, attrs, defStyleAttr) {

    companion object {
        private val TAG = HeaderScrollView::class.java.simpleName
    }

    init {
        overScrollMode = OVER_SCROLL_NEVER
        isMotionEventSplittingEnabled = false
    }

    private var isNeedInvalidate = false
    private var isStickyLayout = false

    private lateinit var headView: ViewGroup
    private lateinit var contentView: ViewGroup

    private var unStickyRunnable: Runnable? = null
    private var stickyRunnable: Runnable? = null

    /**
     * 固定吸顶监听
     */
    private var stickyFixedListener: ((isStickyFixed: Boolean) -> Unit)? = null

    /**
     * 设定 [NestedScrollView] 是否可滚动，默认为可滚动
     */
    private var isScrollable = true
        set(value) {
            if (field == value) return
            field = value
            if (field) {
                stopScroll()
            }
            stickyFixedListener?.invoke(!field)
        }

    /**
     * 是否为吸顶模式
     */
    var isStickyMode = false
        set(value) {
            if (field == value) return
            field = value
            requestLayout()
        }

    /**
     * 是否吸顶固定状态
     */
    fun isStickyFixed(): Boolean {
        return isStickyMode && !isScrollable
    }

    /**
     * 解除吸顶固定，并滑动至顶部
     *
     * @param scrollDurationMs [NestedScrollView] 滑动动画时间
     */
    fun unStickyFixed(scrollDurationMs: Int = 500) {
        if (isScrollable) return
        isScrollable = true

        smoothScrollTo(0, 0, scrollDurationMs)
        removeCallbacks(unStickyRunnable)
        if (unStickyRunnable == null) {
            unStickyRunnable = Runnable {
                //固定吸顶解锁时，使所有RecyclerView置顶，防止HeaderScrollView滑动卡顿
                rvArray.forEach { _, value ->
                    value.get()?.scrollToPosition(0)
                }
            }
        }
        postDelayed(unStickyRunnable, 200)
    }

    /**
     * 进行固定吸顶
     */
    fun stickyFixed(scrollDurationMs: Int = 500) {
        if (!isScrollable) return
        isScrollable = false

        val y = stickyFixedViewTop + 1
        smoothScrollTo(0, y, scrollDurationMs)

        removeCallbacks(stickyRunnable)
        if (stickyRunnable == null) {
            stickyRunnable = Runnable {
                scrollTo(0, y)
            }
        }
        postDelayed(stickyRunnable, scrollDurationMs.toLong())
    }

    /**
     * 设定视图固定吸顶监听
     */
    fun setStickyFixedListener(stickyFixedListener: (isStickyFixed: Boolean) -> Unit) {
        this.stickyFixedListener = stickyFixedListener
    }

    /**
     * 获取头部区域的高度
     */
    private val headViewHeight get() = if (headView.isVisible) headView.measuredHeight else 0

    private val headViewMinHeight get() = if (headView.isVisible) headView.minimumHeight else 0

    /**
     * 固定吸顶的视图Top
     */
    private val stickyFixedViewTop: Int
        get() = (headView as? StickyLinearLayout)?.stickyFixedViewTop ?: 0

    /**
     * 记录上一次固定吸顶后的视图Top
     */
    private var lastStickyFixedViewTop = 0

    /**
     * 嵌套滚动布局的高度
     */
    private val contentViewHeight get() = if (contentView.isVisible) measuredHeight + headViewHeight - headViewMinHeight else measuredHeight

    /**
     * 保存所有[RecyclerView]
     */
    private val rvArray by lazy { SparseArray<WeakReference<RecyclerView>>() }

    override fun onFinishInflate() {
        super.onFinishInflate()
        val childView = getChildAt(0) as ViewGroup
        if (childView.childCount == 2) {
            headView = childView.getChildAt(0) as ViewGroup
            contentView = childView.getChildAt(1) as ViewGroup
            isStickyLayout = headView is StickyLinearLayout

            if (isStickyLayout) {
                headView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
                    (headView as? StickyLinearLayout)?.onScrollChange(
                        this, scrollX, scrollY, scrollX, scrollY
                    )
                }
            }
        } else {
            throw  IllegalStateException("$TAG is designed for nested scrolling and can only have two direct child")
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        if (!isStickyMode) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            return
        }
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        setMeasuredDimension(width, height)
        headView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
        measureChildren()
    }

    private fun measureChildren() {
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            child.measure(
                MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(contentViewHeight, MeasureSpec.EXACTLY)
            )
        }
    }

    override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) {
        if (!isStickyMode) {
            super.onOverScrolled(scrollX, scrollY, clampedX, clampedY)
            return
        }
        val scrollMax = headViewHeight - headView.minimumHeight
        super.onOverScrolled(scrollX, min(scrollMax, scrollY), clampedX, clampedY)
    }

    override fun scrollTo(x: Int, y: Int) {
        val scrollMax = headViewHeight - headView.minimumHeight
        if (!isStickyMode) {
            super.scrollTo(x, min(scrollMax, y))
            return
        }
        if (!isScrollable) {
            scrollToStickyFixedViewTop(x, y)
            return
        }
        if (scrollToStickyFixedViewTop(x, y)) {
            isScrollable = false
            return
        }
        super.scrollTo(x, min(scrollMax, y))
    }

    /**
     * 滑动至吸顶固定视图顶部
     */
    private fun scrollToStickyFixedViewTop(x: Int, y: Int): Boolean {
        val viewTop = stickyFixedViewTop
        //在固定吸顶状态时，HeaderScrollView子控件存在视图刷新，HeaderScrollView将会滑动导致固定吸顶将错位
        if (!isScrollable && viewTop != lastStickyFixedViewTop) {
            lastStickyFixedViewTop = viewTop
            super.scrollTo(x, viewTop)
            return true
        }

        if (isScrollable && viewTop > 0 && y > viewTop) {
            lastStickyFixedViewTop = viewTop
            super.scrollTo(x, viewTop)

            //防止惯性较大，RecyclerView视图持续滚动，保持在第一个Item位置
            findRecyclerView(contentView)?.let {
                if (it.canScrollVertically(1)) {
                    it.scrollToPosition(0)
                }
            }
            return true
        }
        return false
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        if (!isStickyMode) {
            super.onNestedPreScroll(target, dx, dy, consumed, type)
            return
        }
        val isParentScroll = dispatchNestedPreScroll(dx, dy, consumed, null, type)
        if (!isParentScroll) {
            // 向上滑动且当前滑动距离小于顶部视图的高度时，需要此控件滑动响应的距离以保证滑动连贯性
            val scrollMax = headViewHeight - headView.minimumHeight
            val needKeepScroll = dy > 0 && scrollY < scrollMax
            if (needKeepScroll) {
                isNeedInvalidate = true
                scrollBy(0, dy)
                consumed[1] = dy
            }
        }
    }

    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int,
        consumed: IntArray
    ) {
        isNeedInvalidate = true
        super.onNestedScroll(
            target,
            dxConsumed,
            dyConsumed,
            dxUnconsumed,
            dyUnconsumed,
            type,
            consumed
        )
    }

    override fun awakenScrollBars(): Boolean {
        return when {
            !isStickyMode -> super.awakenScrollBars()
            isNeedInvalidate -> {
                invalidate()
                true
            }
            else -> super.awakenScrollBars()
        }
    }

    override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
        super.onScrollChanged(l, t, oldl, oldt)
        if (isStickyMode && isStickyLayout) {
            (headView as? StickyLinearLayout)?.onScrollChange(this, l, t, oldl, oldt)
        }
    }

    override fun fling(velocityY: Int) {
        if (!isStickyMode) {
            super.fling(velocityY)
            return
        }
        // 不要用ScrollView的滚动，用RecyclerView可以保持连贯
        // super.fling(velocityY)
        findRecyclerView(contentView)?.let {
            if (it.canScrollVertically(1)) {
                it.fling(0, velocityY)
            } else {
                super.fling(velocityY)
            }
        } ?: super.fling(velocityY)
    }

    private fun findRecyclerView(contentView: ViewGroup): RecyclerView? {
        if (contentView is RecyclerView && contentView.javaClass == RecyclerView::class.java) {
            if (rvArray.get(contentView.hashCode()) == null) {
                rvArray.put(contentView.hashCode(), WeakReference(contentView))
            }
            return contentView
        }

        var newContentView = contentView
        if (newContentView is ViewPager2) {
            val vpChild = newContentView.getChildAt(0) as? ViewGroup
            val itemCount = newContentView.adapter?.itemCount ?: 0
            if (vpChild != null && itemCount == vpChild.childCount) {
                (vpChild.getChildAt(newContentView.currentItem) as? ViewGroup)?.also {
                    newContentView = it
                }
            }
        }

        for (i in 0 until newContentView.childCount) {
            val view = newContentView.getChildAt(i)
            if (view !is ViewGroup) continue
            val target = findRecyclerView(view)
            if (target != null) {
                return target
            }
        }
        return null
    }

    /**
     * 禁止继续滚动
     */
    fun stopScroll() {
        findRecyclerView(contentView)?.let {
            if (it.scrollState == RecyclerView.SCROLL_STATE_SETTLING) {
                it.stopScroll()
            }
            onStopNestedScroll(it, ViewCompat.TYPE_NON_TOUCH)
        }
    }

    override fun startNestedScroll(axes: Int, type: Int): Boolean {
        if (isStickyMode && isScrollable && type == ViewCompat.TYPE_TOUCH) {
            stopScroll()
        }
        return super.startNestedScroll(axes, type)
    }

    /**
     * 触摸结束，判断当前位置是否达到固定吸顶
     */
    private fun ouTouchOver(event: MotionEvent) {
        when (event.action) {
            MotionEvent.ACTION_DOWN,
            MotionEvent.ACTION_CANCEL,
            MotionEvent.ACTION_UP,
            MotionEvent.ACTION_OUTSIDE -> {
                if (!isScrollable) return
                val viewTop = stickyFixedViewTop
                if (viewTop > 0 && viewTop <= scrollY + 5) {
                    lastStickyFixedViewTop = viewTop
                    super.scrollTo(0, viewTop)
                    isScrollable = false
                }
            }
        }
    }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        ouTouchOver(event)

        if (!isScrollable) return false
        return super.onInterceptTouchEvent(event)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        ouTouchOver(event)

        if (!isScrollable) return false
        return super.onTouchEvent(event)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        removeCallbacks(unStickyRunnable)
        removeCallbacks(stickyRunnable)
        stickyFixedListener = null
    }
}